/*
 *  linux/fs/exec.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * #!-checking implemented by tytso.
 */

/*
 * Demand-loading implemented 01.12.91 - no need to read anything but
 * the header into memory. The inode of the executable is put into
 * "current->executable", and page faults do the actual loading. Clean.
 *
 * Once more I can proudly say that linux stood up to being changed: it
 * was less than 2 hours work to get demand-loading completely implemented.
 */

#include "../include/errno.h"
#include "../include/string.h"
#include "../include/sys/stat.h"

#include "../include/linux/fs.h"
#include "../include/linux/sched.h"
#include "../include/asm/segment.h"
#include "../include/a.out.h"

extern int sys_exit(int exit_code);

extern int sys_close(int fd);

/*
 * MAX_ARG_PAGES defines the number of pages allocated for arguments
 * and envelope for the new program. 32 should suffice, this gives
 * a maximum env+arg of 128kB !
 */
#define MAX_ARG_PAGES 32

int sys_uselib(const char *library) {
    struct m_inode *inode;
    unsigned long base;

    if (get_limit(0x17) != TASK_SIZE)
        return -EINVAL;
    if (library) {
        if (!(inode = namei(library)))        /* get library inode */
            return -ENOENT;
    } else
        inode = NULL;
/* we should check filetypes (headers etc), but we don't */
    iput(current->library);
    current->library = NULL;
    base = get_base(current->ldt[2]);
    base += LIBRARY_OFFSET;
    free_page_tables(base, LIBRARY_SIZE);
    current->library = inode;
    return 0;
}


/*
 * create_tables() parses the env- and arg-strings in new user
 * memory and creates the pointer tables from them, and puts their
 * addresses on the "stack", returning the new stack pointer value.
 */
////在新任务栈中创建参数和环境变量指针表。
// 参数：p-数据段中参数和环境信息偏移指针；
//      argc - 参数个数：
//      enve -环境变量个数。
// 返回：栈指针值。
static unsigned long *create_tables(char *p, int argc, int envc) {
    unsigned long *argv, *envp;
    unsigned long *sp;



    //栈指针是以4字节（1节）为边界进行寻址的，因此这里需让 Sp 为4的整数倍值。
    sp = (unsigned long *) (0xfffffffc & (unsigned long) p);
    // 把 sp 向下（低地址方向）移动，在栈中空出环境变量指针占用的空间，
    // + 1 用于在最后存放一个NULL 值。 sp 将递增指针宽度字节值（4字节）。
    sp -= envc + 1;
    // 让环境变量指针 envp 指向该处。
    envp = sp;
    // 把Sp 向下移动，空出 命令行参数指针占用的空间，
    sp -= argc + 1;
    // 让 argy 指针指向该处
    argv = sp;
    // 此时 sp指向参数指针块的起始处，我们将环境参数块指针 envp和命令行参 数块指针以及命令行参数个数值分别压入栈中。
    put_fs_long((unsigned long) envp, --sp);
    put_fs_long((unsigned long) argv, --sp);
    put_fs_long((unsigned long) argc, --sp);
    //将命令行各参数指针和环境变量各指针分别放入前面空出来的相应地方，
    // 最后分别放置一个 NULL 指针。
    while (argc-- > 0) {
        put_fs_long((unsigned long) p, argv++);
        while (get_fs_byte(p++)) /* nothing */ ;
    }
    put_fs_long(0, argv);
    while (envc-- > 0) {
        put_fs_long((unsigned long) p, envp++);
        while (get_fs_byte(p++)) /* nothing */ ;
    }
    put_fs_long(0, envp);
    return sp;
}

/*
 * count() counts the number of arguments/envelopes
 */
static int count(char **argv) {
    int i = 0;
    char **tmp;

    if ((tmp = argv))
        while (get_fs_long((unsigned long *) (tmp++)))
            i++;

    return i;
}

/*
 * 'copy_string()' copies argument/envelope strings from user
 * memory to free pages in kernel mem. These are in a format ready
 * to be put directly into the top of new user memory.
 *
 * Modified by TYT, 11/24/91 to add the from_kmem argument, which specifies
 * whether the string and the string array are from user or kernel segments:
 *
 * from_kmem     源地址         目标地址
 *    0          user space    user space
 *    1          kernel space  user space
 *    2          kernel space  kernel space
 * 
 * We do this by playing games with the fs segment register.  Since it
 * it is expensive to load a segment register, we try to avoid calling
 * set_fs() unless we absolutely have to.
 */
// 复制指定个数的参数字符串到参数和环境空间中。
// 参数：argc - 欲添加的参数个数：
//      argv -参数指针数组，源数据
//      page -内存页面指针数组，用于存放 参数、环境变量。
//      p-内存空间的偏移指针，这里的内存空间只用于存放 参数、环境变量。 始终指向已复制串的头部；
//      from_kmem -字符串来源标志。
// 在 do_execve () 函数中，p初始化为指向参数表(128kB)空间的最后一个长字处，参数字符串是以堆栈操作方式逆向往其中复制存放的。
// 因此 p指针会随着复制信息的增加而逐渐减小， 并始终指向参数字符串的头部。
// 字符串来源标志 from kmem 应该是 TYT为了给 execve（）增添执行脚本文件的功能而新加的参数。当没有运行脚本文件的功能时，所有参数字符串都在用户数据空间中。
// 返回：参数和环境空间当前头部指针。若出错则返回0。
static unsigned long copy_strings(int argc, char **argv, unsigned long *page,
                                  unsigned long p, int from_kmem) {
    char *tmp, *pag = NULL;
    int len, offset = 0;
    unsigned long old_fs, new_fs;

    if (!p)
        return 0;    /* bullet-proofing */
    // 在进程进入内核态后，fs寄存器默认指向进程的数据段。而ds, es寄存器则指向内核数据段。
    // 在用户态运行时，这些寄存器都指向用户数据段。
    new_fs = get_ds();
    old_fs = get_fs();
    // 如果字符串和字符串数组（指针）来自内核空间，则设置fs 段寄存器指向内核数据段。
    if (from_kmem == 2)
        set_fs(new_fs);
    //循环处理各个参数，从最后一个参数逆向开始复制，复制到指定偏移地址处。
    while (argc-- > 0) {
        // 首先取需要复制的当前字符串指针。
        // 如果从 kernel space 复制字符串到 user space，则设置fs段寄存器指向内核数据段（ds）。
        if (from_kmem == 1)
            set_fs(new_fs);

        // 现在fs一定是指向源数据，从中取得源字符串指针tmp
        if (!(tmp = (char *) get_fs_long(((unsigned long *) argv) + argc)))
            panic("argc is wrong");
        // 若是从 kernel space 复制字符串到 user space，就恢复fs 段寄存器原值（fs再指回用户空间）。
        // 若是从 kernel space 复制字符串到 kernel space，fs还需要指向ds，所以这里也不需处理
        // 若是从 user space 复制字符串到 user space，fs，ds都没有过变化，所以这里也不需处理
        if (from_kmem == 1)
            set_fs(old_fs);
        //此时tmp 指向了源数据

        len = 0;        /* remember zero-padding */
        // 计算该参数字符串长度 len。此后 tmp 指向该字符串末端。
        do {
            len++;
        } while (get_fs_byte(tmp++));
        // 如果该字符串长度超过此时参数和环境空间中还剩余的空闲长度，则空间不够了。于是恢复fs段寄存器值（如果被改变的话）并返回0。
        // 不过因为参数和环境空间留有 128KB，所以通常不可能发生这种情况。
        if (p - len < 0) {    /* this shouldn't happen - 128kB */
            set_fs(old_fs);
            return 0;
        }

        while (len) {
            --p;
            --tmp;
            --len;
            // 偏移量 offset 表示在一个页面中的偏移值，具体是哪个页面由p / PAGE_SIZE决定
            if (--offset < 0) {
                offset = p % PAGE_SIZE;
                if (from_kmem == 2)
                    set_fs(old_fs);
                //申请1页内存页面
                if (!(pag = (char *) page[p / PAGE_SIZE]) &&
                    !(pag = (char *) (page[p / PAGE_SIZE] = get_free_page())))
                    return 0;
                //如果字符串在内核空间，则设置fs 段寄存器指向内核数据段 (ds)
                if (from_kmem == 2)
                    set_fs(new_fs);

            }
            //从fs段中复制字符串的1字节到参数和环境空间内存页面 pag 的 offset 处，
            *(pag + offset) = get_fs_byte(tmp);
        }
    }
    //如果字符串和字符串数组在内核空间，则恢复fs 段寄存器原值。
    if (from_kmem == 2)
        set_fs(old_fs);
    // 最后，返回参数和环境空间中已复制参数的头部偏移值。
    return p;
}

////修改任务的局部描述符表内容。
// 修改局部描述符表 LDT 中描述符的段基址和段限长，并将参数和环境空间页面放置在数据段末端。
// 参数：text_size 一执行文件头部中 a_text 字段给出的代码段长度值：
//      page - 参数和环境空间页面指针数组。
// 返回：数据段限长值 (64MB)
static unsigned long change_ldt(unsigned long text_size, unsigned long *page) {
    unsigned long code_limit, data_limit, code_base, data_base;
    int i;

    code_limit = TASK_SIZE;
    data_limit = TASK_SIZE;
    code_base = get_base(current->ldt[1]);
    data_base = code_base;
    set_base(current->ldt[1], code_base);
    set_limit(current->ldt[1], code_limit);
    set_base(current->ldt[2], data_base);
    set_limit(current->ldt[2], data_limit);
/* make sure fs points to the NEW data segment */
    //fs段寄存器中放入局部表数据段描述符的选择符(0x17)。即默认情况下fs都指向任务数据段。
    __asm__("pushl $0x17\n\tpop %%fs"::);
    data_base += data_limit;
    data_base += data_limit - LIBRARY_SIZE;
    //然后page的32个内存页放到 虚拟地址中数据段的末端。方法是从进程空间末端逆向一页一页地放。
    // 函数 put_page()用于把物理页面映射到进程逻辑空间中。在 mm/memory. c
    for (i = MAX_ARG_PAGES - 1; i >= 0; i--) {
        data_base -= PAGE_SIZE;
        if (page[i])
            put_dirty_page(page[i],data_base);
    }
    return data_limit;
}

/*
 * 'do_execve()' executes a new program.
 *   * NOTE! We leave 4MB free at the top of the data-area for a loadable
 * library.
 */
//// execve()系统中断调用函数。加载并执行子进程（其他程序）
// 该函数是系统中断调用（int 0x80） 功能号 NR_execve 调用的函数。
// do_execve()的参数是进入system_call、sys_execve时逐步压入栈中的值。
// 参数来源：
// 1、第 86--88 行入堆的 edx、ecx 和 ebx 寄存器值，分别对应*envp、**argv 和*filename；
// 2、第94行调用 sys_call_table 中 sys_execve 函数（指针）时压入栈的函数返回地址(tmp）；
// 3、第202行在调用 do_execve() 前入栈的指向栈中调用系统中断的程序代码指针 eip。
// 参数：
// eip - 调用系统中断的程序代码指针，参见 kernel /system_ call. s 程序开始部分的说明；
// tmp - 系统中断 在调用_Sys_execve 时的返回地址，无用；
// filenane — 被执行程序文件名指针；
// argy -命令行参数指针数组的指针；
// envp -环境变量指针数组的指针。
// 返回：如果调用成功，则不返回；否则设置出错号，并返回-1.
int do_execve(unsigned long *eip, long tmp, char *filename,
              char **argv, char **envp) {
    struct m_inode *inode;  //内存中i节点指针。
    struct buffer_head *bh; //高速缓冲块头指针
    struct exec ex;        //执行文件头部数据结构变量
    unsigned long page[MAX_ARG_PAGES]; //参数和环境串空间页面指针数组
    int i, argc, envc;
    int e_uid, e_gid; //有效用户ID和有效组ID
    int retval;       //返回值
    int sh_bang = 0;  //控制是否需要执行脚本程序
    unsigned long p = PAGE_SIZE * MAX_ARG_PAGES - 4; //p指向参数和环境空间的最后部

    //内核准备了 128KB（32个页面）空间来存放执行文件的命令行参数和环境字符串。
    // 上行把p初始设置成位于 128KB空间的最后1个长字处。
    // 在初始参数和环境空间的操作过程中，p将用来指明在128KB 空间中的当前位置。


    // 参数 eip[1]是调用本次系统调用的原用户程序代码段寄存器 CS值，
    // 其中的段选择符 是当前任务的代码段选择符(0x000f）。若不是该值，那么 CS 只能会是内核代码段的选择符 0x0008。
    // 但这是绝对不允许的，因为内核代码是常驻内存而不能被替换掉的。因此根据 eip[1]的值确认是否符合正常情况。
    if ((0xffff & eip[1]) != 0x000f)
        panic("execve called from supervisor mode");

    // 然后再初始化 128KB 的参数和环境串空间，把所有字节清零，
    for (i = 0; i < MAX_ARG_PAGES; i++)    /* clear page-table */
        page[i] = 0;
    // 取出执行文件的i节点。
    if (!(inode = namei(filename)))        /* get executables inode */
        return -ENOENT;
    // 根据函数参数分别计算出命令行参数和环境字符串的个数 arge 和 envc。
    argc = count(argv);
    envc = count(envp);

    restart_interp:
    // 执行文件必须是常规文件。
    if (!S_ISREG(inode->i_mode)) {    /* must be regular file */
        retval = -EACCES;
        goto exec_error2;
    }

    // 根据执行文件i节点中的属性，看看本进程是否有权执行它。
    // 在把执行文件 i节点的属性字段值取到i中后，先查看属性中是否设置了“设置-用户-ID” (set-user_id) 标志和“设置-组-ID” (set-group-id) 标志。
    // 这两个标志主要是让一般用户能够执行特权用户（如超级用户 root）的程序，例如改变密码的程序 passwd 等。
    i = inode->i_mode;
    // 如果 set-user-id 标志置位，则后面执行进程的有效用户 ID (euid)就设置成执行文件的用户ID，否则设置成当前进程的 euid。
    e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
    // 如果 set-group-id 被置位的话，则执行进程的有效组 ID (egid) 就设置为被执行文件的组ID。否则设置成当前进程的egid。
    e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;

    if (current->euid == inode->i_uid)
        // 执行文件属于运行进程的用户，去除Other users' permissions、Group permissions
        i >>= 6;
    else if (in_group_p(inode->i_gid))
        // 执行文件与当前进程的用户属于同组，去除Other users' permissions
        i >>= 3;
    // i所对应的值 是否可以有权运行改文件（位0是执行权限）
    // 属性字i的最低3比特值 来判断当前进程是否有权限运行这个执行文件。
    // suser 是否为超级用户
    if (!(i & 1) &&
        !((inode->i_mode & 0111) && suser())) {
        retval = -ENOEXEC;
        //转到 exec_error2 处去作退出处理。
        goto exec_error2;
    }

    // 取出执行文件头部数据并根据其中的信息来分析设置运行环境，或者运行另一个 shell 程序来执行脚本程序。
    //
    //
    // 通常脚本文件的第一行文本为“#！/bin/bash”。它指明了运行脚本文件需要的解释程序。
    // 运行方法是从脚本文件第1行（带字符’#！”）中取出其中的解释程序名及后面的参数（若有的话），
    // 然后将这些参数和脚本文件名放进执行文件（此时是解释程序）的命令行参数空间中。
    // 在这之前需要先把函数指定的原有命令行参数和环境字符串放到 128KB 空间中，而这里建立起来的命令行参数则放到它们前面位置处（因为是逆向放置）。
    // 最后让内核执行脚本文件的解释 程序。
    // 下面就是在设置好解释程序的脚本文件名等参数后，取出解释程序的i节点并跳转到 204 行去执行解释程序。
    // 由于我们需要跳转到执行过的代码 204 行去，因此在下面确认并处 理了脚本文件之后需要设置一个禁止再次执行下面的脚本处理代码标志 sh bang。
    // 在后面的代码中该标志也用来表示我们己经设置好执行文件的命令行参数，不要重复设置。

    // 首先读取执行文件第1块数据到高速缓冲块中。
    if (!(bh = bread(inode->i_dev, inode->i_zone[0]))) {
        retval = -EACCES;
        goto exec_error2;
    }
    // 复制缓冲块数据到 ex中。
    ex = *((struct exec *) bh->b_data);    /* read exec-header */
    // 如果执行文件开始的两个字节是字符’＃！，则说明执行文件是一个脚本文本文件,执行脚本文件的解释程序（例如 shel1 程序）。
    if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
        /*
         * This section does the #! interpretation.
         * Sorta complicated, but hopefully it will work.  -TYT
         */

        char buf[128], *cp, *interp, *i_name, *i_arg;
        unsigned long old_fs;
        //从脚本文件中提取解释程序名及其参数，并把解释程序名、解释程序的参数和脚本文件名组合放入环境参数块中。
        // 复制脚本文件头1行字符’#！”后面的字符串到 buf中，其中含有脚本解释程序名（例如/bin/sh），也可能还包含解释程序的几个参数。
        strncpy(buf, bh->b_data + 2, 127);
        brelse(bh);
        iput(inode);
        buf[127] = '\0';
        // 删除buf 中开始的空格、制表符。使得脚本内容全部展示在一行上
        // strchr: 在参数 buf 所指向的字符串中搜索第一次出现字符 '\n'（一个无符号字符）的位置。
        if ((cp = strchr(buf, '\n'))) {
            *cp = '\0';
            for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
        }
        if (!cp || *cp == '\0') {
            retval = -ENOEXEC; /* No interpreter name found */
            goto exec_error1;
        }
        //得到了 以解释程序名开头的脚本内容，一行展示（字符串）。
        interp = i_name = cp;
        // 取第一个字符串，它应该是解释程序名，此时 i_name 指向该名称。
        i_arg = 0;
        for (; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
            if (*cp == '/')
                i_name = cp + 1;
        }
        // 若解释程序名后还有字符，则它们应该是解释程序的参数串，于是令 iarg指向该串。
        if (*cp) {
            *cp++ = '\0';  // 解释程序名尾添加 NULL字符。
            i_arg = cp;    // iarg 指向解释程序参数。
        }
        /*
         * OK, we've parsed out the interpreter name and
         * (optional) argument.
         */


        // 把解释程序名 i_name 及其参数 iarg 和脚本文件名作为解释程序的参数放进环境和参数块中。
        // 先把函数提供的 原来的参数和环境字符串先放进去，然后再放这里解析出来的。
        //
        // 例如对于命令行参数来说，如果原来的参数是”-arg1 -arg2”、解释程序名是〞bash”、其参数是”-iargl -iarg2”、脚本文件名（即原来的执行文 件名）是"example.sh”，
        // 那么在放入这里的参数之后，新的命令行类似于这样： “bash -iargl -iarg2 example. sh -arg1 -arg2”
        //
        // 这里把sh_bang标志置上，然后把函数参数提供的原有参数和环境字符串放入到空间中。
        // 环境字符串和参数个数分别是 envc 和 argc-1个。少复制的一个原有参数是原来的执行文件名，即这里的脚本文件名。
        // [[?？可以看出，实际上我们不需要去另行处理脚本文件名，即可以复制argc 个参数，包括原来执行文件名（即现在的脚本文件名）。因为它位于同一个位置上 ]]。
        // 注意！这里指针p随着复制信息增加而逐渐向小地址方向移动，因此这两个复制串函数执行完后，环境参数串信息块位于程序命令行参数串信息块的上方，并且p指向程序的第1个参数串。
        // copy_strings() 最后一个参数（0）指明参数字符串在用户空间。
        if (sh_bang++ == 0) {
            p = copy_strings(envc, envp, page, p, 0);
            p = copy_strings(--argc, argv + 1, page, p, 0);
        }
        /*
         * Splice in (1) the interpreter's name for argv[0]
         *           (2) (optional) argument to interpreter
         *           (3) filename of shell script
         *
         * This is done in reverse order, because of how the
         * user environment and arguments are stored.
         */
        //接着我们逆向复制脚本文件名、解释程序的参数和解释程序文件名到参数和环境空间中。
        // 若出错，则置出错码，跳转到 exec_errorl。
        // 另外，由于本函数参数提供的脚本文件名 filename 在用户空间，而这里赋予 copy_strings() 的脚本文件名指针在内核空间，因此这个复制字符串函数的最后一个参数（字符串来源标志）需要被设置成1。
        // 若字符串在内核空间，则 copy_strings() 的最后一个参数要设置成 2，如下面的第276、279 行。
        p = copy_strings(1, &filename, page, p, 1);
        argc++;
        if (i_arg) {
            //复制解释程序的多个参数。
            p = copy_strings(1, &i_arg, page, p, 2);
            argc++;
        }
        p = copy_strings(1, &i_name, page, p, 2);
        argc++;
        if (!p) {
            retval = -ENOMEM;
            goto exec_error1;
        }
        /*
         * OK, now restart the process with the interpreter's inode.
         */
        //最后我们取得解释程序的i节点指针，然后跳转到 204 行去执行解释程序。
        // 为了获得解释程序的i节点，我们需要使用 namei()函数，但是该函数所使用的参数（文件名）是从用户数据空间得到的，即从段寄存器fs 所指空间中取得。
        // 因此在调用 namei (） 函数之前,先临时让fs 指向内核数据空间，以让函数能从内核空间得到解释程序名，
        old_fs = get_fs();
        set_fs(get_ds());
        if (!(inode = namei(interp))) { /* get executables inode */
            set_fs(old_fs);
            retval = -ENOENT;
            goto exec_error1;
        }
        // 恢复 fs 的默认设置, 并跳转到 restart_interp 重新处理新的执行文件—— 脚本文件的解释程序。
        set_fs(old_fs);
        goto restart_interp;
    }

    // 此时缓冲块中的解释程序的头结构数据己经复制到了 ex 中。
    // 于是先释放该缓冲块，并开始对 ex中的执行头信息进行判断处理。
    brelse(bh);
    // 对于 Linux 0.11 内核来说，它仅支持 ZMAGIC执行文件格式，并且执行文件代码都从逻辑地址-0开始执行，因此不支持含有代码或数据重定位信息的执行文件。
    //下列情况将不执行程序：如果执行文件的a_magic不是ZMAGIC、或者代码和数据重定位部分长度等于 0、
    // 或者(代码段+数据段+堆）长度超过 50MB、或者执行文件长度小于（代码段+数据段+符号表长度+执行头部分）长度的总和。
    if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
        ex.a_text + ex.a_data + ex.a_bss > 0x3000000 ||
        inode->i_size < ex.a_text + ex.a_data + ex.a_syms + N_TXTOFF(ex)) {
        retval = -ENOEXEC;
        goto exec_error2;
    }
    //如果执行文件中代码开始处没有位于1个页面(1024字节）边界处，则也不能执行。
    //因为需求页(Demand paging)技术要求加载执行文件内容时以页面为单位，因此要求执行文件映像中代码和数据都从页面边界处开始。
    if (N_TXTOFF(ex) != BLOCK_SIZE) {
        printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
        retval = -ENOEXEC;
        goto exec_error2;
    }
    // 如果 sh_bang == 0，则复制指定个数的命令行参数和环境字符串到参数和环境空间中。
    // 若 sh_bang != 0，则表明是将运行脚本解释程序，此时环境变量页面己经复制，无须再复制。
    if (!sh_bang) {
        // 指针p随着复制信息增加而逐渐向小地址方向移动， 因此这两个复制串函数执行完后，环境参数串信息块位于程序参数串信息块的上方，并且p指向程序的第1 个参数串。
        p = copy_strings(envc, envp, page, p, 0);
        p = copy_strings(argc, argv, page, p, 0);
        // 事实上，p是128KB内存中的偏移值。因此如果 p=0，则表示环境变量与参数空间页面己经被占满，容纳不下了。
        if (!p) {
            retval = -ENOMEM;
            goto exec_error2;
        }
    }
/* OK, This is the point of no return */
/* note that current->library stays unchanged by an exec */
    // 先放回进程原执行程序的i节点，
    if (current->executable)
        iput(current->executable);
    // 让进程 executable 字段指向新执行文件的i节点。
    current->executable = inode;
    current->signal = 0;
    // 复位原进程的所有信号处理句柄。
    for (i=0 ; i<32 ; i++) {
        current->sigaction[i].sa_mask = 0;
        current->sigaction[i].sa_flags = 0;
        //对于 SIGIGN 句柄无须复位, 这是<<内核完全注释>>一书上的建议
        if (current->sigaction[i].sa_handler != SIG_IGN)
            current->sigaction[i].sa_handler = NULL;
    }

    // 根据设定的执行时关闭文件句柄 (close_on_exec）位图标志，关闭指定的打开文件，并
    for (i = 0; i < NR_OPEN; i++)
        if ((current->close_on_exec >> i) & 1)
            sys_close(i);
    // 复位标志。
    current->close_on_exec = 0;
    // 根据当前进程指定的基地址和限长，释放原来程序的代码段和数据段所对应的内存页表 指定的物理内存页面及页表本身。
    // 此时新执行文件并没有占用主内存区任何页面，因此在处理器真正运行新执行文件代码时就会引起缺页异常中断，
    // 此时内存管理程序即会执行缺页处理而为新执行文件申请内存页面和设置相关页表项，并且把相关执行文件页面读入内存中。
    free_page_tables(get_base(current->ldt[1]), get_limit(0x0f));
    free_page_tables(get_base(current->ldt[2]), get_limit(0x17));
    //如果“上次任务使用了协处理器”指向的是当前进程，则将其置空，并复位使用了协处理器的标志。
    if (last_task_used_math == current)
        last_task_used_math = NULL;
    current->used_math = 0;
    //根据新执行文件头结构中的代码长度字段 atext 的值修改局部表中描述符基址和段限长，并将128KB 的内存页(参数和环境空间页面)放置在数据段末端。
    p += change_ldt(ex.a_text, page) - MAX_ARG_PAGES * PAGE_SIZE;
    p -= LIBRARY_SIZE + MAX_ARG_PAGES*PAGE_SIZE;
    // 执行下面语句之后，p改为以数据段起始处为原点的偏移值，但仍指向参数和环境空间数据开始处，即己转换成为栈指针值。
    // create_tables 在栈空间中创建环境和参数变量指针表，供程序的 main(） 作为参数使用，并返回该栈指针。
    p = (unsigned long) create_tables((char *) p, argc, envc);
    //修改进程各字段值为新执行文件的信息。
    // 进程任务结构代码尾字段 end_code = 执行文件的代码段长度 a_text；
    // 数据尾字段 end_data = 执行文件的代码段长度 + 数据段长度（ a_text + a_data)
    // 进程堆结尾字段 brk = a_text + a_data + a_bss。brk 用于指明进程当前数据段（包括未初始化数据部分）末端位置。
    current->brk = ex.a_bss +
                   (current->end_data = ex.a_data +
                                        (current->end_code = ex.a_text));
    // 设置进程栈开始字段为栈指针所在页面，
    current->start_stack = p & 0xfffff000;

    // 设置进程的有效用户 id 和有效组 id。
    current->suid = current->euid = e_uid;
    current->sgid = current->egid = e_gid;

    //最后将原调用系统中断的程序在堆栈上的代码指针替换为指向新执行程序的入口点，
    eip[0] = ex.a_entry;        /* eip, magic happens :-) */
    // 将栈指针替换为新执行文件的栈指针。此后返回指令将弹出这些栈数据并使得CPU 去执行新执行文件，因此不会返回到原调用系统中断的程序中去了。
    eip[3] = p;            /* stack pointer */
    return 0;
    exec_error2:
    iput(inode);
    exec_error1:
    for (i = 0; i < MAX_ARG_PAGES; i++)
        free_page(page[i]);
    return (retval);
}